﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FLib;
using TagLib;
using LibraryGuard.Interfaces;

namespace LibraryGuard.Internals
{
	/// <summary>
	/// Default data provider.
	/// </summary>
	internal class DefaultDataProvider
		: IDataProvider
	{
		#region Config
		private const string DefaultSeparator = "/";
		#endregion

		#region Private fields
		private static readonly Dictionary<string, Variable> VariableBindings = new Dictionary<string, Variable>();
		private static readonly Dictionary<string, MethodInfo> FunctionBindings = new Dictionary<string, MethodInfo>();
		#endregion

		#region Static constructor
		static DefaultDataProvider()
		{
			VariableBindings.Add("artist", new Variable(TypeCode.String, t => new AST.String(t.Tag.Performers.Length > 0 ? t.Tag.Performers[0] : string.Empty)));
			VariableBindings.Add("artists", new Variable(TypeCode.String, t => new AST.String(string.Join(DefaultSeparator, t.Tag.Performers))));
			VariableBindings.Add("title", new Variable(TypeCode.String, t => new AST.String(t.Tag.Title)));
			VariableBindings.Add("album", new Variable(TypeCode.String, t => new AST.String(t.Tag.Album)));
			VariableBindings.Add("year", new Variable(TypeCode.Int32, t => new AST.Number((int)t.Tag.Year)));
			VariableBindings.Add("genere", new Variable(TypeCode.String, t => new AST.String(t.Tag.Genres.Length > 0 ? t.Tag.Genres[0] : string.Empty)));
			VariableBindings.Add("generes", new Variable(TypeCode.String, t => new AST.String(string.Join(DefaultSeparator, t.Tag.Genres))));
			VariableBindings.Add("composer", new Variable(TypeCode.String, t => new AST.String(t.Tag.Composers.Length > 0 ? t.Tag.Composers[0] : string.Empty)));
			VariableBindings.Add("composers", new Variable(TypeCode.String, t => new AST.String(string.Join(DefaultSeparator, t.Tag.Composers))));
			VariableBindings.Add("albumartists", new Variable(TypeCode.String, t => new AST.String(string.Join(DefaultSeparator, t.Tag.AlbumArtists))));
			VariableBindings.Add("track", new Variable(TypeCode.Int32, t => new AST.Number((int)t.Tag.Track)));
			VariableBindings.Add("totaltracks", new Variable(TypeCode.Int32, t => new AST.Number((int)t.Tag.TrackCount)));
			VariableBindings.Add("disc", new Variable(TypeCode.Int32, t => new AST.Number((int)t.Tag.Disc)));
			VariableBindings.Add("totaldiscs", new Variable(TypeCode.Int32, t => new AST.Number((int)t.Tag.DiscCount)));
			VariableBindings.Add("comment", new Variable(TypeCode.String, t => new AST.String(t.Tag.Comment)));
			VariableBindings.Add("extension", new Variable(TypeCode.String, t => new AST.String(System.IO.Path.GetExtension(t.Name).Remove(0, 1))));

			var methods = typeof(DefaultFunctions).GetMethods()
				.Where(m => m.IsStatic && m.IsPublic);
			foreach (var m in methods)
				FunctionBindings.Add(m.Name.ToLower(), m);
		}
		#endregion

		#region Properties
		/// <summary>
		/// Current file information.
		/// </summary>
		public File CurrentFile { get; set; }
		#endregion

		#region IDataProvider Members
		/// <inheritdoc />
		public IValue GetVariable(string name)
		{
			Variable variable = null;
			if (!VariableBindings.TryGetValue(name.ToLower(), out variable))
				throw new ArgumentException("Variable '{0}' doesn't exist.".FormatWith(name));
			return variable.Binder(this.CurrentFile);
		}

		/// <inheritdoc />
		public IValue CallFunction(string name, params IValue[] args)
		{
			MethodInfo m = null;
			if (!FunctionBindings.TryGetValue(name.ToLower(), out m))
				throw new ArgumentException("Function '{0}' doesn't exist.".FormatWith(name));

			return this.TryCall(m, args);
		}

		/// <inheritdoc />
		public TypeCode GetFunctionReturnType(string name)
		{
			MethodInfo m = null;
			if (!FunctionBindings.TryGetValue(name.ToLower(), out m))
				throw new ArgumentException("Function '{0}' doesn't exist.".FormatWith(name));

			return Type.GetTypeCode(m.ReturnType);
		}

		/// <inheritdoc />
		public TypeCode GetVariableType(string name)
		{
			Variable variable = null;
			if (!VariableBindings.TryGetValue(name.ToLower(), out variable))
				throw new ArgumentException("Variable '{0}' doesn't exist.".FormatWith(name));
			return variable.Type;
		}
		#endregion

		#region Auxiliary methods
		private IValue TryCall(MethodInfo m, IValue[] args)
		{
			var pms = m.GetParameters();
			if (args.Length != pms.Length)
				throw new ArgumentException("Function {0} doesn't take {1} arguments.".FormatWith(m.Name, args.Length));
			var objs = new object[args.Length];
			for (int i = 0; i < args.Length; i++)
			{
				if (!args[i].CanBeConvertedTo(Type.GetTypeCode(pms[i].ParameterType), this))
					throw new ArgumentException("Argument {0} in function {1} has invalid type.".FormatWith(i, m.Name));
				objs[i] = args[i].GetValue(Type.GetTypeCode(pms[i].ParameterType), this);
			}
			var ret = m.Invoke(null, objs);
			switch (Type.GetTypeCode(m.ReturnType))
			{
				case TypeCode.String:
					return new AST.String((string)ret);

				case TypeCode.Int32:
					return new AST.Number((int)ret);

				case TypeCode.Boolean:
					return new AST.Boolean((bool)ret);
			}
			return new AST.String(ret.ToString());
		}
		#endregion

		#region Variable desc
		private class Variable
		{
			public readonly TypeCode Type = TypeCode.Object;
			public readonly Func<File, IValue> Binder = null;

			public Variable(TypeCode type, Func<File, IValue> binder)
			{
				this.Type = type;
				this.Binder = binder;
			}
		}
		#endregion
	}
}
